
系列:30 天打造企業級 PLM|面向:後端
三年前出貨的產品出問題,品保要調閱「當時那一版」的規格與 BOM。版本管理的價值,在事故發生那天才真正顯現。Day 3 定了主檔薄、版本厚、版本不可變的模型,今天講它跑起來的機制:改版不是改資料,是長出一個新版本;而且從草稿到發行的整段路,正式資料一個字都不能被動到。
品項頁實機畫面(版本狀態、生命週期與各分頁):

在 Oracle Agile PLM 中,版本變更(ECO Affected Items)是由後端 EJB 核心深度管轄的。
當料號被掛入變更單時,Agile 會在後端產生一個 Pending Revision,並對該物件施加強烈的「變更鎖定(Change Lock)」。這套舊架構有著經典痛點:
Mini-PLM 採用實體隔離的預備版本(Prepared Revision)模式:以乾淨的兩個 JPA 實體徹底隔開「正式世界」與「簽核平行世界」,放行時單一交易原子合併,架構純粹且無殘留鎖定風險。
Day 3 講過 FormItemLink.pending_data 暫存簽核中的欄位修改;版本這端的對應機制是預備版本:品項掛進變更單時,先從目前版本長出一個 Draft 狀態的新版本(實碼節錄,FormItemLinkService):
// 若指定版號已存在且不是原版號,直接回 409,避免後續拋 500
if (!isTemporaryRevisionNumber(newRevNumber)
&& itemRevisionRepository.findByItemAndRevisionNumber(item, newRevNumber).isPresent()) {
throw new ConflictException("版本號已存在: " + newRevNumber);
}
ItemRevision preparedRevision = itemRevisionService.createPreparedRevisionForForm(
revision, newRevNumber,
normalizeText(request.getTargetLifecyclePhase()),
revision.getDescription(), currentUser, form.getFormNumber());
link.setNewRevision(preparedRevision);
於是簽核期間同時存在兩個世界。正式世界是 is_latest 的 Released 版本,所有人查到的都是它;平行世界是掛在 FormItemLink 上的 Draft 預備版本,只在變更單的上下文可見。簽核中的所有編輯,欄位、BOM redline、AML redline,都作用在預備版本上。表單放行那一刻,預備版本轉正(Released、is_latest 換手、原版本卸下 latest);駁回則整個平行世界丟棄。
**「簽核中」與「已生效」的隔離不是用旗標,是用兩個實體。**這比同一列資料加個 pending 欄位乾淨得多,因為兩個世界各自有完整的欄位、BOM 與附件。
base_revision_id 自參照記錄這版從哪版長出來,族譜靠結構不靠 log。版號策略:使用者可指定(A、B、C…),未指定時先給暫時版號、放行時定案。指定版號撞號在掛單當下就擋 409,fail fast,不是等放行才炸 500。
表單放行時,每個 Affected Item 要在一個交易裡完成六件事:
1. pending_data 落入預備版本欄位
2. 預備版本 BOM / AML redline 定案
3. 預備版本 → RELEASED、蓋 release_date
4. is_latest 換手(舊版卸下、新版掛上)
5. mp_item.latest_revision_id 更新(查詢捷徑同步)
6. 依 targetLifecyclePhase 設定生命週期階段
一致性約束跟 Day 9 的 returnToPending 同款,清單少一條就出鬼。latest_revision_id 是為查詢效能加的反正規化捷徑,列表頁不用每次 MAX() 找最新版,代價就是第 5 步永遠不能漏。反正規化的規矩向來如此:加捷徑的人,要負責所有寫入路徑的同步。
預備版本機制的邊角案例比主流程多得多。真實案發:品項從變更單移除(link 變 REMOVED)時,早期版本沒有清掉掛在 link 上的預備版本。這個 Draft 版本成了孤兒,佔著版號。使用者下次把同一顆料掛進新變更單、指定同一個版號:409 版本號已存在。「明明沒有 B 版,為什麼說 B 版存在?」因為有一個看不見的 B 版孤兒。
修法除了補上移除時清理,還加了一道防禦性自我修復(實碼):
// 加入前先修復歷史殘留(REMOVED 關聯殘留 newRevision、孤兒預備版本)
repairResidualPreparedRevisionsBeforeAdd(item);
每次掛品項進表單前,先掃一遍該品項的歷史殘留並修復。這是企業系統的務實選擇:bug 修好了,但已經在生產資料庫裡的髒資料不會自己消失。與其寫一次性清理 SQL 賭它掃得乾淨,不如把修復邏輯內建在下一次操作的入口,讓系統自我痊癒。
改版機制的核心是平行世界:預備版本隔離簽核中與已生效,放行是原子的世界合併,反正規化捷徑人人有責,孤兒資料靠自我修復收拾。版本講完了,明天講掛在版本上的那棵樹。Day 14:BOM 多階展開,後端演算法與前端樹狀表格。